<!DOCTYPE html>
<!--
Copyright 2017 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/components/app-route/app-route.html">
<link rel="import" href="/components/paper-toast/paper-toast.html">
<link rel="import" href="/components/paper-tooltip/paper-tooltip.html">

<link rel="import" href="/elements/base-style.html">
<link rel="import" href="/elements/cancel-job-dialog.html">
<link rel="import" href="/elements/job-page/change-details.html">
<link rel="import" href="/elements/job-page/exception-details.html">
<link rel="import" href="/elements/job-page/job-chart.html">
<link rel="import" href="/elements/job-page/job-details.html">
<link rel="import" href="/elements/job-page/job-menu-fab.html">
<link rel="import" href="/elements/loading-wrapper.html">
<link rel="import" href="/elements/results2-frame.html">

<dom-module id="job-page">
  <template>
    <style include="base-style">
      #failed {
        background: var(--paper-red-50);
        color: var(--paper-red-500);
        cursor: pointer;
      }

      #failed-tooltip {
        max-width: 50em;
        white-space: pre-wrap;
      }

      #running {
        background: var(--paper-indigo-50);
        color: var(--paper-indigo-500);
      }

      #queued {
        background: var(--paper-grey-50);
        color: var(--paper-grey-500);
      }

      #cancelled {
        background: var(--paper-orange-50);
        color: var(--paper-orange-500);
      }

      h1 {
        margin-bottom: 0.1em;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      #layout {
        display: grid;
        grid-auto-columns: 1fr;
        grid-column-gap: 24px;
        grid-row-gap: 24px;
        grid-template-rows: max-content;
        margin-top: 24px;
      }

      #details-column {
        grid-column: 1 / 3;
        grid-row: 1 / 3;
      }

      #results {
        grid-column: 3 / 9;
      }

      #change-a {
        grid-column: 3 / 6;
      }

      #change-a > div {
        background-color: var(--paper-indigo-500);
        height: 8px;
      }

      #change-b {
        grid-column: 6 / 9;
      }

      #change-b > div {
        background-color: var(--paper-pink-a200);
        height: 8px;
      }

      #exception {
        color: var(--paper-grey-500);
        white-space: pre-wrap;
      }
    </style>

    <app-route route="{{route}}" pattern="/:jobId" data="{{routeData}}"></app-route>

    <loading-wrapper url="/api/job/[[routeData.jobId]]?o=STATE&amp;o=ESTIMATE" response="{{job}}">
      <h1>
        <template is="dom-if" if="[[failed(job)]]">
          <span id="failed" class="badge">Error</span>
          <paper-tooltip for="failed" animation-delay="0">
            <div id="failed-tooltip">[[job.exception]]</div>
          </paper-tooltip>
        </template>
        <template is="dom-if" if="[[running(job)]]">
          <span id="running" class="badge">Running</span>
          <cancel-job-dialog job="[[job]]"
            auth-headers="[[authHeaders]]"></cancel-job-dialog>
        </template>
        <template is="dom-if" if="[[cancelled(job)]]">
          <span id="cancelled" class="badge">Cancelled</span>
          <paper-tooltip for="cancelled" animation-delay="0">
            <div id="cancelled-tooltip">[[job.cancel_reason]]</div>
          </paper-tooltip>
        </template>
        <template is="dom-if" if="[[queued(job)]]">
          <span id="queued" class="badge">Queued</span>
          <cancel-job-dialog job="[[job]]"
            auth-headers="[[authHeaders]]"></cancel-job-dialog>
        </template>
        [[job.name]]
      </h1>
      <p class="byline">
        For [[job.user]]
        <span class="middle-dot"></span>
        created on [[createTime(job)]]
        <span class="middle-dot"></span>
        waiting time: [[queueDuration(job)]]
        <span class="middle-dot"></span>
        started on [[startTime(job)]]
        <span class="middle-dot"></span>
        running time: [[runDuration(job)]]
      </p>
      <p class="byline">
        In queue: <a href="[[formatQueueUrl(job.configuration)]]">[[job.configuration]]</a>
      </p>
      <div id="layout">
        <div id="details-column">
          <job-details job="[[job]]"></job-details>
        </div>
        <div id="results">
          <template is="dom-if" if="[[failed(job)]]">
            <h2>Job failed</h2>

            <exception-details exception="[[job.exception]]"></exception-details>
          </template>
          <template is="dom-if" if="[[hasEstimate(job)]]">
            <h2 id="estimate">Estimated Job Time</h2>
            <h3>
              Similar jobs in the past have taken approximately [[historicMedian(job)]] &#177; [[historicStdDev(job)]] and [[historicP90(job)]] at the 90th percentile.
            </h3>
            <h3>
              Currently there are [[queueAhead(job)]] jobs ahead of this one in the <a href="[[formatQueueUrl(job.configuration)]]">[[job.configuration]]</a> queue. Expected waiting time is [[queueWaitingTime(job)]].
            </h3>
          </template>
          <template is="dom-if" if="[[shouldShowChart(job)]]">
            <job-chart job="[[job]]" change-index="{{changeIndex}}"></job-chart>
          </template>
          <template is="dom-if" if="[[!failed(job)]]">
            <template is="dom-if" if="[[isTryJob(job)]]">
              <results2-frame job-id="[[job.job_id]]"></results2-frame>
            </template>
          </template>
        </div>
        <div id="change-a">
          <div></div>
          <change-details job="[[job]]" change-state="[[previousChangeState(job.state, changeIndex)]]" extra-args="[[getExtraArgs(job, 0)]]"></change-details>
        </div>
        <div id="change-b">
          <div></div>
          <change-details job="[[job]]" change-state="[[changeState(job.state, changeIndex)]]" extra-args="[[getExtraArgs(job, 1)]]"></change-details>
        </div>
      </div>
      <job-menu-fab job="[[job]]" auth-headers="[[authHeaders]]"></job-menu-fab>
    </loading-wrapper>
  </template>

  <script>
    'use strict';
    Polymer({
      is: 'job-page',

      properties: {
        job: {
          type: Object,
          observer: '_jobChanged',
        },

        changeIndex: {
          type: Number
        }
      },

      getExtraArgs(job, pos) {
        if (!this.isTryJob(job)) {
          return null;
        }
        let a = job.arguments;
        if (pos === 0) {
          return a.base_extra_args;
        }
        return a.experiment_extra_args;
      },

      _jobChanged() {
        this.setChangeIndex();
      },

      setChangeIndex() {
        if (!this.job) {
          return;
        }
        // TODO: Choose the largest difference, not just any difference.
        for (let i = 0; i < this.job.state.length; ++i) {
          if (this.job.state[i].comparisons.prev === 'different') {
            this.set('changeIndex', i);
            return;
          }
        }
        // TODO: If no statistical difference, choose the largest delta.
        this.set('changeIndex', this.job.state.length - 1);
      },

      isTryJob(job) {
        if (!job) {
          return false;
        }
        return !job.comparison_mode || job.comparison_mode === 'try';
      },

      failed(job) {
        if (!job) {
          return false;
        }
        return job.status.toLowerCase() === 'failed';
      },

      running(job) {
        if (!job) {
          return false;
        }
        return job.status.toLowerCase() === 'running';
      },

      cancelled(job) {
        if (!job) {
          return false;
        }
        return job.status.toLowerCase() === 'cancelled';
      },

      queued(job) {
        if (!job) {
          return false;
        }
        return job.status.toLowerCase() === 'queued';
      },

      formatDate(dateString) {
        /** We want to use a subset of the ISO format to keep datetimes
          * consistent independent of the user's locale, but still present a
          * localized date.
          */
        function pad(n) {
          if (n < 10) {
            return '0' + n;
          }
          return n;
        }

        const d = new Date(dateString + 'Z');
        return d.getFullYear() +
          '-' + pad(d.getMonth() + 1) +
          '-' + pad(d.getDate()) +
          ' T' + pad(d.getHours()) +
          ':' + pad(d.getMinutes()) +
          ':' + pad(d.getSeconds());
      },

      createTime(job) {
        if (!job) {
          return 'N/A';
        }
        return this.formatDate(job.created);
      },

      startTime(job) {
        if (!job || job.started_time === null) {
          return 'N/A';
        }
        return this.formatDate(job.started_time);
      },

      formatQueueUrl(configuration) {
        return '/queue-stats/' + configuration;
      },

      queueAhead(job) {
        let ahead = 0;
        if (!this.hasEstimate(job)) {
          return ahead;
        }
        for (const s of job.queue_stats.job_id_with_status) {
          if (s.job_id == job.job_id) {
            break;
          }
          ahead += 1;
        }
        return ahead;
      },

      queueWaitingTime(job) {
        let normalizedWait = 0.0;
        let count = 0;
        if (!this.hasEstimate(job)) {
          return 'unknown';
        }
        for (const s of job.queue_stats.queue_time_samples) {
          const seconds = s[0] * 3600.0;
          const position = s[1];
          if (!position) {
            continue;
          }

          normalizedWait += seconds / position;
          count += 1;
        }
        if (count === 0) {
          return 'unknown';
        }

        normalizedWait /= count;

        const currentPosition = this.queueAhead(job);

        return this.formatTimedelta(normalizedWait * currentPosition);
      },

      formatTimedelta(seconds) {
        let h = Math.floor(seconds / 3600);
        let m = Math.floor((seconds % 3600) / 60);

        if (h < 10) {
          h = '0' + h;
        }
        if (m < 10) {
          m = '0' + m;
        }
        return h + ':' + m + ' hours';
      },

      hasEstimate(job) {
        return job && 'estimate' in job;
      },

      historicMedian(job) {
        if (!this.hasEstimate(job)) {
          return 'unknown';
        }
        return this.formatTimedelta(job.estimate.timings[0]);
      },

      historicStdDev(job) {
        if (!this.hasEstimate(job)) {
          return 'unknown';
        }
        return this.formatTimedelta(job.estimate.timings[1]);
      },

      historicP90(job) {
        if (!this.hasEstimate(job)) {
          return 'unknown';
        }
        return this.formatTimedelta(job.estimate.timings[2]);
      },

      formatDuration(durationMs) {
        const seconds = durationMs / 1000;
        if (seconds < 60) {
          return seconds.toFixed(1) + ' seconds';
        }

        const minutes = durationMs / (1000 * 60);
        if (minutes < 60) {
          return minutes.toFixed(1) + ' minutes';
        }

        const hours = durationMs / (1000 * 60 * 60);
        return hours.toFixed(1) + ' hours';
      },

      queueDuration(job) {
        if (!job) {
          return 'unknown';
        }
        const created = new Date(job.created + 'Z');

        if (job.started_time === null) {
          if (this.cancelled(job)) {
            // Since we don't have a start time, this means it was
            // cancelled in the queue.
            const updated = new Date(job.updated + 'Z');
            return this.formatDuration(updated - created);
          }

          // We assume this job is still in the queue.
          return this.formatDuration(new Date() - created);
        }

        const started = new Date(job.started_time + 'Z');
        const durationMs = started - created;
        return this.formatDuration(durationMs);
      },

      runDuration(job) {
        if (!job) {
          return 'unknown';
        }

        if (job.started_time === null) {
          // This means we haven't started yet, so leave this 0.
          return this.formatDuration(0);
        }

        const started = new Date(job.started_time + 'Z');
        const updated = new Date(job.updated + 'Z');
        const durationMs = updated - started;
        return this.formatDuration(durationMs);
      },

      shouldShowChart(job) {
        if (!job || !job.comparison_mode || job.comparison_mode === 'try') {
          return false;
        }
        return job.state.some(state => state.result_values.length);
      },

      previousChangeState(jobState, changeIndex) {
        return jobState[changeIndex - 1];
      },

      changeState(jobState, changeIndex) {
        return jobState[changeIndex];
      },

      selected() {
        this.$$('loading-wrapper').$.request.generateRequest();
      },
    });
  </script>
</dom-module>
